Repository navigation
fix(cli): bound the loopback Studio probe so a dropped connection costs 1 s, not 10 - #6466
Conversation
…ts 1 s, not 10 Some hosts drop a connection to a closed loopback port instead of refusing it (WSL 2 mirrored networking, a firewall in stealth mode). The Vite Studio probe on 127.0.0.1:5190 then waits for fetch's 10 s connect timeout, and since every render, check and snapshot looks for a Studio through the history owner, each of them paid 10 s. Aborting the fetch does not help: the connect attempt keeps its socket, and the process, alive for the full 10 s. The default probe now connects with node:net under a 1 s bound and destroys the socket, then fetches only from a host that accepted.
jrusso1020
left a comment
There was a problem hiding this comment.
Reviewed at 5a20b67ddd7445109569e65f0a436b3b847256a4.
I checked both IPv4 and IPv6 discovery callers. The preliminary TCP connect is bounded and destroyed on success, timeout or error before fetch starts. This narrows the blackholed-connect case; it does not impose a universal deadline on the later fetch.
vitest studioSelectionClient.test.ts: 15 passed. This bounds the preliminary TCP connect, not response-body or later socket races. Those were already unbounded; no firewall emulation performed.
Need: yes · Size: ok · Reuse: existing implementation seam · Simpler: already minimal · Structure: fine
Risk: medium - socket cleanup during loopback discovery.
Verdict: APPROVE
Reasoning: I checked both IPv4 and IPv6 discovery callers. The preliminary TCP connect is bounded and destroyed on success, timeout or error before fetch starts. This narrows the blackholed-connect case; it does not impose a universal deadline on the later fetch.
Reasoning: I checked both IPv4 and IPv6 discovery callers. The preliminary TCP connect is bounded and destroyed on success, timeout or error before fetch starts. This narrows the blackholed-connect case; it does not impose a universal deadline on the later fetch.
What
The loopback Studio probe in
findPreviewServerForProjectconnects withnode:netunder a 1 s bound and destroys the socket before it fetches/api/projects. A host that drops the connection now costs 1 s instead of 10 s.Why
Some hosts drop a connection to a closed loopback port instead of refusing it: WSL 2 with
networkingMode=mirrored, or a firewall in stealth mode. On those hostsfetch("http://127.0.0.1:5190/api/projects")waits for undici's 10 s connect timeout. Everyrender,checkandsnapshotlooks for a Studio through the history owner, so each of them paid that 10 s.Adding an
AbortSignal.timeoutto the fetch is not enough. The fetch rejects on time, but the connect attempt keeps its socket open, and that keeps the process alive for the same 10 s. Anetsocket that is destroyed when its timeout fires lets the process exit at once.To reproduce, on WSL 2 with mirrored networking and nothing on port 5190:
Related work
Also WSL: #5560, #5574, #5591 (renders fall back to software GPU under WSL). Fixes #6465.
How
fetchLoopback(url): connects to the URL's host and port withnode:net. It rejects and destroys the socket after 1 s, on an error, or once connected. After a successful connect it returnsfetch(url).fetchImploffindPreviewServerForProject. The parameter type narrows to(url: string) => Promise<Response>, which is the only way it is called. Callers and tests that inject their own fetch are unchanged.Test plan
fetchLoopbackfetches from a listening loopback server, and rejects without fetching when the port refuses the connection.studioSelectionClient.test.ts: 15/15 pass.hyperframes@0.8.205with this change applied to its dist:check: 13.0 s → 4.2 s (warm)fetch http://127.0.0.1:5190/api/projects, traced through a--requirehookoxlintandoxfmt --checkare clean on both files.tsc --noEmitinpackages/cliis clean afterbun run build.history.test.ts("a CLI run waits while another process holds the history…" and "serializes concurrent begin --exclusive claims…") time out at 20 s on this machine with and without this change. They are unrelated to it.